#include "Buffer.h"
#include "Camera.h"
#include "Texture.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/matrix_operation.hpp>
#include <iostream>

#include <memory>
namespace Umikaze::Core {
struct DirectionalLight {
  glm::vec4 _direction;
  glm::vec4 _colorAndIntensity;
  DirectionalLight() {}
};

struct PointLight {
  glm::vec4 _color;
  glm::vec4 _positionAndRadius;

  PointLight() {}
};

struct SpotLight {
  glm::vec4 _color;
  glm::vec4 _positionAndLength;
  glm::vec4 _directionAndInnerAngle;

  SpotLight() {}
};

class Lights {
public:
  void setAmbient(const glm::vec4 &color) { _lightData._ambientColor = color; }

  void addDirectionalLight(const DirectionalLight &d) {
    if (_lightData._directionalLightCount > 3) {
      std::cerr << "直线光太多啦" << std::endl;
      return;
    }
    auto &light =
        _lightData._directionalLights[_lightData._directionalLightCount++];
    auto normalDirection = glm::normalize(glm::vec3(d._direction));
    light._direction = glm::vec4(normalDirection, 1.0f);
    light._colorAndIntensity = d._colorAndIntensity;
  }

  void addPointLight(const PointLight &p) {
    if (_lightData._pointLightCount > 3) {
      std::cerr << "点光源太多啦" << std::endl;
      return;
    }
    _lightData._pointLights[_lightData._directionalLightCount++] = p;
  }

  void addSpotLight(const SpotLight &s) {
    if (_lightData._spotLightCount > 3) {
      std::cerr << "聚光灯太多啦" << std::endl;
      return;
    }
    _lightData._spotLights[_lightData._spotLightCount++] = s;
  }

  // TODO：实际上不希望这一层直接去依赖uniformBlock，
  //最好这里再在core空间里抽象个中间组件出来，
  //但是目前封装得算是比较干净，不会导致snippet依赖opengl
  //所以先暂时用着
  std::shared_ptr<Buffer> getLightData() {
    if (!_lightBuffer) {
      _lightBuffer = std::make_shared<Buffer>();
    }
    _lightBuffer->set(&_lightData, sizeof(LightData));
    return _lightBuffer;
  }

  void resetLight() {
    _lightData._ambientColor = glm::vec4(0, 0, 0, 1);
    _lightData._directionalLightCount = 0;
    _lightData._spotLightCount = 0;
    _lightData._pointLightCount = 0;
  }

  glm::mat4
  getDirectionalLightSpaceMatrix(uint32_t index,
                                 const std::shared_ptr<Camera> &camera) {
    if (index >= _lightData._directionalLightCount) {
      std::cerr << "没有这个light" << std::endl;
    }

    auto &light = _lightData._directionalLights[index];

    auto direction = glm::vec3(light._direction);

    auto view = camera->getViewMatrix();
    auto proj = camera->getProjMatrix();

    return getDirectionalLightSpaceMatrix(view, proj, direction);
  }

  std::vector<glm::mat4>
  getDirectionalLightSpaceMatrixCascade(uint32_t index,
                                        const std::shared_ptr<Camera> &camera,
                                        std::vector<float> &splits) {
    std::vector<glm::mat4> ret;

    auto lightDir = glm::vec3(_lightData._directionalLights[index]._direction);

    auto splitNear = camera->_near;
    auto farPlane = camera->_far;
    auto fov = camera->_fov;
    auto aspect = camera->_aspect;

    auto view = camera->getViewMatrix();

    for (uint32_t i = 0; i < splits.size(); ++i) {

      auto splitFar = farPlane * splits[i];
      auto proj = glm::perspective(fov, aspect, splitNear, splitFar);
      ret.push_back(getDirectionalLightSpaceMatrix(view, proj, lightDir));
      splitNear = splitFar;
    }
    return ret;
  }

  struct LightData {
    glm::vec4 _ambientColor;
    int _directionalLightCount = 0;
    int _spotLightCount = 0;
    int _pointLightCount = 0;
    int pad;
    DirectionalLight _directionalLights[4];
    PointLight _pointLights[4];
    SpotLight _spotLights[4];
  } _lightData;

  std::shared_ptr<Buffer> _lightBuffer;

private:
  std::vector<glm::vec4> getFrustumCornersWorldSpace(const glm::mat4 &proj,
                                                     const glm::mat4 &view) {
    const auto inv = glm::inverse(proj * view);
    std::vector<glm::vec4> frustumCorners;
    for (uint32_t x = 0; x < 2; ++x) {
      for (uint32_t y = 0; y < 2; ++y) {
        for (uint32_t z = 0; z < 2; ++z) {
          const glm::vec4 pt = inv * glm::vec4(2.0f * x - 1.0f, 2.0f * y - 1.0f,
                                               2.0f * z - 1.0f, 1.0f);
          frustumCorners.push_back(pt / pt.w);
        }
      }
    }
    return frustumCorners;
  }

  glm::mat4 getDirectionalLightSpaceMatrix(const glm::mat4 &view,
                                           const glm::mat4 &proj,
                                           const glm::vec3 &lightDir) {

    auto corners = getFrustumCornersWorldSpace(proj, view);

    auto tempCenter = glm::vec4(0.0);

    for (auto &v : corners) {
      tempCenter += v;
    }
    tempCenter /= corners.size();

    auto center = glm::vec3(tempCenter);

    auto up = glm::vec3(0.0, 1.0, 0.0);
    if (glm::abs(glm::dot(lightDir, glm::vec3(0.0, 1.0, 0.0))) > 0.999) {
      up = glm::vec3(1.0, 0.0, 0.0);
    }

    auto lightView = glm::lookAt(center - lightDir, center, up);

    float minX = std::numeric_limits<float>::max();
    float maxX = std::numeric_limits<float>::min();
    float minY = std::numeric_limits<float>::max();
    float maxY = std::numeric_limits<float>::min();
    float minZ = std::numeric_limits<float>::max();
    float maxZ = std::numeric_limits<float>::min();
    for (const auto &v : corners) {
      const auto trf = lightView * v;
      minX = std::min(minX, trf.x);
      maxX = std::max(maxX, trf.x);
      minY = std::min(minY, trf.y);
      maxY = std::max(maxY, trf.y);
      minZ = std::min(minZ, trf.z);
      maxZ = std::max(maxZ, trf.z);
    }

    // Tune this parameter according to the scene
    constexpr float zMult = 10.0f;
    if (minZ < 0) {
      minZ *= zMult;
    } else {
      minZ /= zMult;
    }
    if (maxZ < 0) {
      maxZ /= zMult;
    } else {
      maxZ *= zMult;
    }

    const glm::mat4 lightProjection =
        glm::ortho(minX, maxX, minY, maxY, minZ, maxZ);

    return lightProjection * lightView;
  }
};

} // namespace Umikaze::Core
